/*
  Live.js - One script closer to Designing in the Browser
  Written for Handcraft.com by Martin Kool (@mrtnkl).

  Version 4.
  Recent change: Made stylesheet and mimetype checks case insensitive.

  http://livejs.com
  http://livejs.com/license (MIT)  
  @livejs

  Include live.js#css to monitor css changes only.
  Include live.js#js to monitor js changes only.
  Include live.js#html to monitor html changes only.
  Mix and match to monitor a preferred combination such as live.js#html,css  

  By default, just include live.js to monitor all css, js and html changes.
  
  Live.js can also be loaded as a bookmarklet. It is best to only use it for CSS then,
  as a page reload due to a change in html or css would not re-include the bookmarklet.
  To monitor CSS and be notified that it has loaded, include it as: live.js#css,notify
*/
(function () {

  var headers = { "Etag": 1, "Last-Modified": 1, "Content-Length": 1, "Content-Type": 1 },
	  resources = {},
	  pendingRequests = {},
	  currentLinkElements = {},
	  oldLinkElements = {},
	  interval = 1000,
	  loaded = false,
	  active = { "html": 1, "css": 1, "js": 1 };

  var Live = {

	// performs a cycle per interval
	heartbeat: function () {	  
	  if (document.body) {		
		// make sure all resources are loaded on first activation
		if (!loaded) Live.loadresources();
		Live.checkForChanges();
	  }
	  setTimeout(Live.heartbeat, interval);
	},

	// loads all local css and js resources upon first activation
	loadresources: function () {

	  // helper method to assert if a given url is local
	  function isLocal(url) {
		var loc = document.location,
			reg = new RegExp("^\\.|^\/(?!\/)|^[\\w]((?!://).)*$|" + loc.protocol + "//" + loc.host);
		return url.match(reg);
	  }

	  // gather all resources
	  var scripts = document.getElementsByTagName("script"),
		  links = document.getElementsByTagName("link"),
		  uris = [];

	  // track local js urls
	  for (var i = 0; i < scripts.length; i++) {
		var script = scripts[i], src = script.getAttribute("src");
		if (src && isLocal(src))
		  uris.push(src);
		if (src && src.match(/\blive.js#/)) {
		  for (var type in active)
			active[type] = src.match("[#,|]" + type) != null
		  if (src.match("notify")) 
			alert("Live.js is loaded.");
		}
	  }
	  if (!active.js) uris = [];
	  if (active.html) uris.push(document.location.href);

	  // track local css urls
	  for (var i = 0; i < links.length && active.css; i++) {
		var link = links[i], rel = link.getAttribute("rel"), href = link.getAttribute("href", 2);
		if (href && rel && rel.match(new RegExp("stylesheet", "i")) && isLocal(href)) {
		  uris.push(href);
		  currentLinkElements[href] = link;
		}
	  }

	  // initialize the resources info
	  for (var i = 0; i < uris.length; i++) {
		var url = uris[i];
		Live.getHead(url, function (url, info) {
		  resources[url] = info;
		});
	  }

	  // add rule for morphing between old and new css files
	  var head = document.getElementsByTagName("head")[0],
		  style = document.createElement("style"),
		  rule = "transition: all .3s ease-out;"
	  css = [".livejs-loading * { ", rule, " -webkit-", rule, "-moz-", rule, "-o-", rule, "}"].join('');
	  style.setAttribute("type", "text/css");
	  head.appendChild(style);
	  style.styleSheet ? style.styleSheet.cssText = css : style.appendChild(document.createTextNode(css));

	  // yep
	  loaded = true;
	},

	// check all tracking resources for changes
	checkForChanges: function () {
	  for (var url in resources) {
		if (pendingRequests[url])
		  continue;

		Live.getHead(url, function (url, newInfo) {
		  var oldInfo = resources[url],
			  hasChanged = false;
		  resources[url] = newInfo;
		  for (var header in oldInfo) {
			// do verification based on the header type
			var oldValue = oldInfo[header],
				newValue = newInfo[header],
				contentType = newInfo["Content-Type"];
			switch (header.toLowerCase()) {
			  case "etag":
				if (!newValue) break;
				// fall through to default
			  default:
				hasChanged = oldValue != newValue;
				break;
			}
			// if changed, act
			if (hasChanged) {
			  Live.refreshResource(url, contentType);
			  break;
			}
		  }
		});
	  }
	},

	// act upon a changed url of certain content type
	refreshResource: function (url, type) {
	  switch (type.toLowerCase()) {
		// css files can be reloaded dynamically by replacing the link element							   
		case "text/css":
		  var link = currentLinkElements[url],
			  html = document.body.parentNode,
			  head = link.parentNode,
			  next = link.nextSibling,
			  newLink = document.createElement("link");

		  html.className = html.className.replace(/\s*livejs\-loading/gi, '') + ' livejs-loading';
		  newLink.setAttribute("type", "text/css");
		  newLink.setAttribute("rel", "stylesheet");
		  newLink.setAttribute("href", url + "?now=" + new Date() * 1);
		  next ? head.insertBefore(newLink, next) : head.appendChild(newLink);
		  currentLinkElements[url] = newLink;
		  oldLinkElements[url] = link;

		  // schedule removal of the old link
		  Live.removeoldLinkElements();
		  break;

		// check if an html resource is our current url, then reload							   
		case "text/html":
		  if (url != document.location.href)
			return;

		  // local javascript changes cause a reload as well
		case "text/javascript":
		case "application/javascript":
		case "application/x-javascript":
		  document.location.reload();
	  }
	},

	// removes the old stylesheet rules only once the new one has finished loading
	removeoldLinkElements: function () {
	  var pending = 0;
	  for (var url in oldLinkElements) {
		// if this sheet has any cssRules, delete the old link
		try {
		  var link = currentLinkElements[url],
			  oldLink = oldLinkElements[url],
			  html = document.body.parentNode,
			  sheet = link.sheet || link.styleSheet,
			  rules = sheet.rules || sheet.cssRules;
		  if (rules.length >= 0) {
			oldLink.parentNode.removeChild(oldLink);
			delete oldLinkElements[url];
			setTimeout(function () {
			  html.className = html.className.replace(/\s*livejs\-loading/gi, '');
			}, 100);
		  }
		} catch (e) {
		  pending++;
		}
		if (pending) setTimeout(Live.removeoldLinkElements, 50);
	  }
	},

	// performs a HEAD request and passes the header info to the given callback
	getHead: function (url, callback) {
	  pendingRequests[url] = true;
	  var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XmlHttp");
	  xhr.open("HEAD", url, true);
	  xhr.onreadystatechange = function () {
		delete pendingRequests[url];
		if (xhr.readyState == 4 && xhr.status != 304) {
		  xhr.getAllResponseHeaders();
		  var info = {};
		  for (var h in headers) {
			var value = xhr.getResponseHeader(h);
			// adjust the simple Etag variant to match on its significant part
			if (h.toLowerCase() == "etag" && value) value = value.replace(/^W\//, '');
			if (h.toLowerCase() == "content-type" && value) value = value.replace(/^(.*?);.*?$/i, "$1");
			info[h] = value;
		  }
		  callback(url, info);
		}
	  }
	  xhr.send();
	}
  };

  // start listening
  if (document.location.protocol != "file:") {
	if (!window.liveJsLoaded)
	  Live.heartbeat();

	window.liveJsLoaded = true;
  }
  else if (window.console)
	console.log("Live.js doesn't support the file protocol. It needs http.");	
})();